import { jest } from '@jest/globals';
import { createServer } from 'http';
import { Server } from 'socket.io';
import { io as Client } from 'socket.io-client';
import ConnectionManager from '../../websocket/ConnectionManager.js';
import { mockLogger, testUtils } from '../setup.js';

// 模拟Socket.IO
jest.mock('socket.io');

const createMockRoomManager = () => ({
  createRoom: jest.fn(),
  joinRoom: jest.fn(),
  leaveRoom: jest.fn(),
  toggleReady: jest.fn(),
  startGame: jest.fn(),
  getAvailableRooms: jest.fn(),
  updatePlayerStatus: jest.fn()
});

const createMockGameEngine = () => ({
  startGame: jest.fn(),
  playCards: jest.fn(),
  passTurn: jest.fn(),
  collectCards: jest.fn(),
  initiateKouLe: jest.fn(),
  respondToKouLe: jest.fn(),
  getGameState: jest.fn()
});

const createMockSocket = () => {
  const mockSocket = {
    id: 'mock-socket-id',
    join: jest.fn(),
    leave: jest.fn(),
    emit: jest.fn(),
    to: jest.fn().mockReturnThis(),
    disconnect: jest.fn(),
    on: jest.fn(),
    removeAllListeners: jest.fn()
  };
  
  // 模拟事件处理
  const eventHandlers = new Map();
  mockSocket.on.mockImplementation((event, handler) => {
    eventHandlers.set(event, handler);
  });
  
  mockSocket._trigger = (event, ...args) => {
    const handler = eventHandlers.get(event);
    if (handler) {
      return handler(...args);
    }
  };
  
  return mockSocket;
};

const createMockSocketIO = () => ({
  on: jest.fn(),
  to: jest.fn().mockReturnThis(),
  emit: jest.fn(),
  close: jest.fn(),
  sockets: {
    sockets: new Map()
  }
});

describe('ConnectionManager', () => {
  let connectionManager;
  let mockServer;
  let mockRoomManager;
  let mockGameEngine;
  let mockSocketIO;
  let mockSocket;

  beforeEach(() => {
    mockServer = createServer();
    mockRoomManager = createMockRoomManager();
    mockGameEngine = createMockGameEngine();
    mockSocketIO = createMockSocketIO();
    mockSocket = createMockSocket();

    // 模拟Socket.IO构造函数
    Server.mockImplementation(() => mockSocketIO);

    connectionManager = new ConnectionManager(
      mockServer,
      mockRoomManager,
      mockGameEngine
    );

    // 模拟logger
    connectionManager.logger = mockLogger;

    // 清理定时器
    jest.clearAllTimers();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.clearAllMocks();
    jest.useRealTimers();
    connectionManager?.close();
  });

  describe('initialization', () => {
    it('should initialize WebSocket server correctly', () => {
      expect(Server).toHaveBeenCalledWith(mockServer, expect.objectContaining({
        cors: expect.objectContaining({
          origin: expect.arrayContaining(['http://localhost:5173']),
          methods: ['GET', 'POST'],
          credentials: true
        }),
        transports: ['websocket', 'polling']
      }));
      
      expect(mockSocketIO.on).toHaveBeenCalledWith('connection', expect.any(Function));
    });

    it('should start heartbeat monitoring', () => {
      expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 60 * 1000);
    });
  });

  describe('connection handling', () => {
    let connectionHandler;

    beforeEach(() => {
      connectionHandler = mockSocketIO.on.mock.calls.find(
        call => call[0] === 'connection'
      )[1];
    });

    it('should handle new connection', () => {
      connectionHandler(mockSocket);

      expect(connectionManager.connections.has(mockSocket.id)).toBe(true);
      expect(mockSocket.on).toHaveBeenCalledWith('register_player', expect.any(Function));
      expect(mockSocket.on).toHaveBeenCalledWith('create_room', expect.any(Function));
      expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function));
    });

    describe('register_player event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
      });

      it('should register player successfully', async () => {
        const mockCallback = jest.fn();
        const playerData = { playerId: 'test-player', roomId: 'TEST01' };
        
        mockRoomManager.updatePlayerStatus.mockResolvedValue(true);
        mockGameEngine.getGameState.mockResolvedValue({ round: 1 });
        
        await mockSocket._trigger('register_player', playerData, mockCallback);

        expect(connectionManager.playerConnections.get('test-player')).toBe(mockSocket.id);
        expect(mockSocket.join).toHaveBeenCalledWith('TEST01');
        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({ success: true })
        );
      });

      it('should reject registration without player ID', async () => {
        const mockCallback = jest.fn();
        
        await mockSocket._trigger('register_player', {}, mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'INVALID_PLAYER'
            })
          })
        );
      });

      it('should disconnect old socket for same player', async () => {
        const mockCallback = jest.fn();
        const oldSocket = createMockSocket();
        oldSocket.id = 'old-socket';
        
        mockSocketIO.sockets.sockets.set('old-socket', oldSocket);
        connectionManager.playerConnections.set('test-player', 'old-socket');

        await mockSocket._trigger('register_player', { playerId: 'test-player' }, mockCallback);

        expect(oldSocket.disconnect).toHaveBeenCalledWith(true);
        expect(connectionManager.playerConnections.get('test-player')).toBe(mockSocket.id);
      });
    });

    describe('create_room event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
      });

      it('should create room successfully', async () => {
        const mockCallback = jest.fn();
        const roomData = { roomName: 'Test Room', playerName: 'TestHost' };
        const mockResult = {
          success: true,
          data: {
            room: { id: 'TEST01', name: 'Test Room' },
            player: { id: 'host-id', name: 'TestHost' }
          }
        };

        mockRoomManager.createRoom.mockResolvedValue(mockResult);
        mockRoomManager.updatePlayerStatus.mockResolvedValue(true);

        await mockSocket._trigger('create_room', roomData, mockCallback);

        expect(mockRoomManager.createRoom).toHaveBeenCalledWith('Test Room', 'TestHost');
        expect(mockSocket.join).toHaveBeenCalledWith('TEST01');
        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({ success: true })
        );
      });

      it('should reject room creation with invalid data', async () => {
        const mockCallback = jest.fn();

        await mockSocket._trigger('create_room', {}, mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'INVALID_INPUT'
            })
          })
        );
      });

      it('should handle room creation failure', async () => {
        const mockCallback = jest.fn();
        const roomData = { roomName: 'Test Room', playerName: 'TestHost' };

        mockRoomManager.createRoom.mockResolvedValue({
          success: false,
          message: '房间创建失败'
        });

        await mockSocket._trigger('create_room', roomData, mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'CREATE_FAILED'
            })
          })
        );
      });
    });

    describe('join_room event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
      });

      it('should join room successfully', async () => {
        const mockCallback = jest.fn();
        const joinData = { roomId: 'TEST01', playerName: 'TestPlayer' };
        const mockResult = {
          success: true,
          data: {
            room: { id: 'TEST01', name: 'Test Room' },
            player: { id: 'player-id', name: 'TestPlayer', seatIndex: 1 }
          }
        };

        mockRoomManager.joinRoom.mockResolvedValue(mockResult);
        mockRoomManager.updatePlayerStatus.mockResolvedValue(true);

        await mockSocket._trigger('join_room', joinData, mockCallback);

        expect(mockRoomManager.joinRoom).toHaveBeenCalledWith('TEST01', 'TestPlayer');
        expect(mockSocket.join).toHaveBeenCalledWith('TEST01');
        expect(mockSocket.to).toHaveBeenCalledWith('TEST01');
        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({ success: true })
        );
      });

      it('should handle join failure', async () => {
        const mockCallback = jest.fn();
        const joinData = { roomId: 'TEST01', playerName: 'TestPlayer' };

        mockRoomManager.joinRoom.mockResolvedValue({
          success: false,
          message: '房间已满'
        });

        await mockSocket._trigger('join_room', joinData, mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'JOIN_FAILED'
            })
          })
        );
      });
    });

    describe('start_game event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
        connectionManager.connections.get(mockSocket.id).playerId = 'host-id';
        connectionManager.connections.get(mockSocket.id).roomId = 'TEST01';
      });

      it('should start game successfully', async () => {
        const mockCallback = jest.fn();
        const mockRoomResult = {
          success: true,
          data: {
            room: {
              id: 'TEST01',
              players: [
                { id: 'host-id', name: 'Host' },
                { id: 'player-1', name: 'Player1' },
                { id: 'player-2', name: 'Player2' }
              ]
            }
          }
        };
        const mockGameResult = {
          success: true,
          data: { sessionId: 'session-123', gameState: { round: 1 } }
        };

        mockRoomManager.startGame.mockResolvedValue(mockRoomResult);
        mockGameEngine.startGame.mockResolvedValue(mockGameResult);

        await mockSocket._trigger('start_game', mockCallback);

        expect(mockRoomManager.startGame).toHaveBeenCalledWith('host-id');
        expect(mockGameEngine.startGame).toHaveBeenCalledWith(
          'TEST01',
          mockRoomResult.data.room.players
        );
        expect(mockSocketIO.to).toHaveBeenCalledWith('TEST01');
        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({ success: true })
        );
      });

      it('should reject start game for non-room player', async () => {
        const mockCallback = jest.fn();
        connectionManager.connections.get(mockSocket.id).playerId = null;

        await mockSocket._trigger('start_game', mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'NOT_IN_ROOM'
            })
          })
        );
      });
    });

    describe('play_cards event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
        connectionManager.connections.get(mockSocket.id).playerId = 'player-id';
        connectionManager.connections.get(mockSocket.id).roomId = 'TEST01';
      });

      it('should play cards successfully', async () => {
        const mockCallback = jest.fn();
        const mockCards = [{ id: 'card-1', rank: '10' }];
        const mockResult = {
          success: true,
          data: { gameState: { round: 1 } }
        };

        mockGameEngine.playCards.mockResolvedValue(mockResult);

        await mockSocket._trigger('play_cards', { cards: mockCards }, mockCallback);

        expect(mockGameEngine.playCards).toHaveBeenCalledWith(
          'TEST01',
          'player-id',
          mockCards
        );
        expect(mockSocketIO.to).toHaveBeenCalledWith('TEST01');
        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({ success: true })
        );
      });

      it('should handle play cards failure', async () => {
        const mockCallback = jest.fn();
        const mockCards = [{ id: 'card-1', rank: '10' }];

        mockGameEngine.playCards.mockResolvedValue({
          success: false,
          error: { code: 'INVALID_PLAY', message: 'Invalid card play' }
        });

        await mockSocket._trigger('play_cards', { cards: mockCards }, mockCallback);

        expect(mockCallback).toHaveBeenCalledWith(
          expect.objectContaining({
            success: false,
            error: expect.objectContaining({
              code: 'PLAY_FAILED'
            })
          })
        );
      });
    });

    describe('disconnect event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
        connectionManager.connections.get(mockSocket.id).playerId = 'test-player';
        connectionManager.connections.get(mockSocket.id).roomId = 'TEST01';
        connectionManager.playerConnections.set('test-player', mockSocket.id);
      });

      it('should handle disconnection properly', () => {
        mockRoomManager.updatePlayerStatus.mockResolvedValue(true);

        mockSocket._trigger('disconnect', 'client disconnect');

        expect(mockRoomManager.updatePlayerStatus).toHaveBeenCalledWith(
          'test-player',
          'offline'
        );
        expect(mockSocketIO.to).toHaveBeenCalledWith('TEST01');
        expect(connectionManager.connections.has(mockSocket.id)).toBe(false);
      });

      it('should set timeout for player removal', () => {
        mockSocket._trigger('disconnect', 'client disconnect');

        // 快进30秒
        jest.advanceTimersByTime(30000);

        // 验证超时处理被调用
        expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 30000);
      });
    });

    describe('ping event', () => {
      beforeEach(() => {
        connectionHandler(mockSocket);
      });

      it('should handle ping correctly', () => {
        const mockCallback = jest.fn();
        const connectionBefore = connectionManager.connections.get(mockSocket.id);
        const lastPingBefore = connectionBefore.lastPing;

        mockSocket._trigger('ping', mockCallback);

        const connectionAfter = connectionManager.connections.get(mockSocket.id);
        expect(connectionAfter.lastPing).not.toBe(lastPingBefore);
        expect(mockCallback).toHaveBeenCalledWith('pong');
      });
    });
  });

  describe('heartbeat monitoring', () => {
    it('should cleanup timeout connections', () => {
      const timeoutSocket = createMockSocket();
      timeoutSocket.id = 'timeout-socket';
      
      mockSocketIO.sockets.sockets.set('timeout-socket', timeoutSocket);
      connectionManager.connections.set('timeout-socket', {
        socket: timeoutSocket,
        playerId: null,
        roomId: null,
        connectedAt: new Date(),
        lastPing: new Date(Date.now() - 6 * 60 * 1000) // 6分钟前
      });

      // 触发心跳检查
      jest.advanceTimersByTime(60 * 1000);

      expect(timeoutSocket.disconnect).toHaveBeenCalledWith(true);
    });

    it('should not cleanup active connections', () => {
      const activeSocket = createMockSocket();
      activeSocket.id = 'active-socket';
      
      mockSocketIO.sockets.sockets.set('active-socket', activeSocket);
      connectionManager.connections.set('active-socket', {
        socket: activeSocket,
        playerId: null,
        roomId: null,
        connectedAt: new Date(),
        lastPing: new Date() // 刚刚ping
      });

      // 触发心跳检查
      jest.advanceTimersByTime(60 * 1000);

      expect(activeSocket.disconnect).not.toHaveBeenCalled();
    });
  });

  describe('utility methods', () => {
    it('should return correct stats', () => {
      connectionManager.connections.set('socket1', { roomId: 'TEST01', playerId: 'player1' });
      connectionManager.connections.set('socket2', { roomId: 'TEST02', playerId: 'player2' });
      connectionManager.connections.set('socket3', { roomId: null, playerId: null });
      connectionManager.playerConnections.set('player1', 'socket1');
      connectionManager.playerConnections.set('player2', 'socket2');

      const stats = connectionManager.getStats();

      expect(stats).toEqual({
        totalConnections: 3,
        totalPlayers: 2,
        connectedRooms: 2
      });
    });

    it('should broadcast system message', () => {
      const message = 'Server maintenance';
      const level = 'warning';

      connectionManager.broadcastSystemMessage(message, level);

      expect(mockSocketIO.emit).toHaveBeenCalledWith(
        'system_message',
        expect.objectContaining({
          success: true,
          data: expect.objectContaining({
            message,
            level,
            timestamp: expect.any(String)
          })
        })
      );
    });

    it('should close server properly', () => {
      connectionManager.close();

      expect(mockSocketIO.close).toHaveBeenCalled();
    });
  });

  describe('error handling', () => {
    let connectionHandler;

    beforeEach(() => {
      connectionHandler = mockSocketIO.on.mock.calls.find(
        call => call[0] === 'connection'
      )[1];
      connectionHandler(mockSocket);
    });

    it('should handle room manager errors gracefully', async () => {
      const mockCallback = jest.fn();
      mockRoomManager.createRoom.mockRejectedValue(new Error('Database error'));

      await mockSocket._trigger('create_room', 
        { roomName: 'Test Room', playerName: 'TestHost' }, 
        mockCallback
      );

      expect(mockCallback).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.objectContaining({
            code: 'SERVER_ERROR'
          })
        })
      );
    });

    it('should handle game engine errors gracefully', async () => {
      const mockCallback = jest.fn();
      connectionManager.connections.get(mockSocket.id).playerId = 'player-id';
      connectionManager.connections.get(mockSocket.id).roomId = 'TEST01';
      
      mockGameEngine.playCards.mockRejectedValue(new Error('Game error'));

      await mockSocket._trigger('play_cards', 
        { cards: [{ id: 'card-1' }] }, 
        mockCallback
      );

      expect(mockCallback).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.objectContaining({
            code: 'SERVER_ERROR'
          })
        })
      );
    });
  });
});